24 個瀏覽器端到端測試,24 個通過。
桌機、平板、手機都能從入口一路走到最後;重新整理後,先前的步驟也能恢復。端到端測試,就是像使用者一樣,真的從瀏覽器操作畫面、呼叫後端,再檢查整條路徑有沒有照預期走完。
我原本以為,今天最難的部分會是把「從心出發」第一次做成看得見、按得到的介面。
結果最後讓我停下來的,不是畫面,也不是心理支持候選的選擇邏輯,而是一個只有三個字母的 HTTP 方法:PUT。
安全審查發現,我替請求內容設下的大小限制有執行,卻只守住 POST;偏偏介面裡用來修正理解、記錄練習、寫下反思與安排追蹤的更新,多數走的是 PUT。
保護確實存在,但它沒有覆蓋真正正在走的路。
Day 03 寫的是「記得之後怎麼辦」:狀態怎麼修正、資料怎麼保存與撤回、來源版本怎麼失效,以及一個候選為什麼可以或不可以進到下一步。那時的重點是離線契約,不是可供真人使用的心理產品。
今天我往前走了一小步:把這些已存在的本機能力接成一段可以操作的旅程。
使用者可以先寫下一句話,再檢查系統整理出的情境、感受線索與需要;如果理解不對,可以修改。接著,系統從受限制的合成資料中取得候選,讓使用者選擇、拒絕、做一個很短的練習、留下反思,再設定下一次回來的提醒。這些步驟會保存在本機 session,也可以刪除。
這條路徑還會實際呼叫固定版本的本機 ARCI 執行鏈。ARCI 在這裡負責檢索與治理,也就是不只找出相近內容,還要檢查來源、版本、撤回狀態與使用資格;它不是心理師,也不負責診斷。高風險句子則走固定安全路徑,不呼叫一般模型,也不呼叫 ARCI。
這一輪沒有使用真人心理資料,沒有呼叫正式模型,也沒有部署。畫面裡看見的內容和驗收案例都是本機合成情境。
前端最容易吸走注意力。
背景雨勢有動畫,聲音預設靜音,使用者必須自己開啟;手機版不能橫向溢出,鍵盤操作要能跳到主要內容,重新整理後也要回到正確階段。最後的瀏覽器驗收在三種畫面尺寸各跑八項,共 24/24 通過,而且沒有靠重試把紅燈洗成綠燈。
這些結果很重要,但它們回答的是「正常操作能不能完成」。在測試裡,這種一路照預期往前的路徑常被叫做 happy path。
安全邊界問的問題不一樣:如果送進來的資料不是正常大小呢?如果它甚至不是應該接收的 JSON 呢?如果呼叫的是更新資料的路由,而不是建立資料的路由呢?
我的 API 前面已經有一層共用防線。它不相信請求標頭自己宣告的大小,而是實際計算收到多少 bytes;接收時間超過五秒就停止,內容超過 32 KB 就拒絕,格式不是 JSON 也不繼續。
單看這段描述,我很容易以為「請求大小已經有限制」。真正的問題藏在進入防線之前:當時只有方法等於 POST,才會走進這段檢查。
POST 常用來建立新資料;PUT 常用來更新既有資料。它們只是 HTTP 請求的不同方法,但對中介層來說,方法不同就可能走到不同分支。這次建立 session 用 POST,修正理解、練習、反思與追蹤卻用 PUT。於是同一個使用者旅程,入口受到大小限制,後面的幾個更新入口卻沒有受到同一層 app-level 限制。
這不是「畫面顯示錯誤」,也不是「候選答案算錯」。安全審查把它分類為修補前的 Medium finding:在這個限定為本機的版本中,本機呼叫者可以用過大的 PUT body 繞過應用程式這一層的 byte boundary。
我沒有證據說它曾被外部利用,也沒有真人資料外洩的紀錄。把「可以繞過一層限制」寫成「已經遭到攻擊」,會是另一種不誠實。
我後來用一個比較容易記住的方式理解這個錯誤。
假設一棟房子的大門有行李尺寸檢查,從大門進來的人都會被檢查。但房子還有幾條側邊走廊;如果規則只寫「從大門進來要檢查」,那道設備即使做得再正確,也沒有保護走側邊的人。
POST 和 PUT 就像不同入口。問題不在檢查器不會算大小,而在我用錯了它的適用範圍。
flowchart TD
A[瀏覽器送出資料] --> B{HTTP 方法}
B -->|POST| C[原本已進入大小與時間限制]
B -->|PUT| D[原本漏過 app-level body cap]
C --> E[JSON 類型檢查]
D --> F[修補:PUT 走同一條防線]
F --> E
E --> G[五秒接收上限]
G --> H[32 KB actual-body cap]
H --> I[再交給資料 schema 與旅程邏輯]
Schema 是資料欄位、型別和長度必須遵守的規則。它仍然需要,但不能取代前面的 raw body 邊界:要進到欄位驗證,伺服器已經先接收請求內容。兩層在回答不同問題,一層限制「最多收多少」,另一層檢查「收到的資料長什麼樣子」。
最省事的假修法,是告訴自己「反正現在只跑在本機」,或者把更新功能改回比較容易通過既有防線的樣子。這些做法都沒有回答真正的問題:為什麼同樣帶 body 的請求,會得到不同保護?
最後的修改很小。原本的判斷是:只有 POST 進入 bounded receive;修補後,POST、PUT、PATCH 都進入同一條既有路徑。
我沒有拿掉更新功能,沒有縮短正常 journey,也沒有讓測試忽略超大內容。原本的 JSON 檢查、五秒接收上限與 32 KB actual-body cap 都保留,只是把它們套到真正會承載內容的方法上。
然後我加了一個專門針對 PUT 的回歸測試。回歸測試的意思是:今天修好問題後,未來再改程式時,也要確認這個行為沒有被弄壞。測試先送錯誤的 media type,確認回應是 415;再送超過邊界的內容,確認回應是 413。這兩個狀態碼分別表示「不接受這種內容格式」和「請求內容太大」。
修補後,完整 repository 的離線回歸是 963/963 通過;瀏覽器端到端測試仍是 24/24。這些數字沒有證明系統對真人有療效,也沒有證明部署環境安全。它們只證明,在這次受控範圍內,新增防線沒有破壞既有離線行為,而且已知的 PUT 遺漏有對應測試守住。
如果只看元件清單,我早就可以勾選「有 request limit」「有 schema」「有安全 middleware」。但清單沒有告訴我,每一條真正的路徑是否都會經過它們。
這是 Day 04 和 Day 03 對我最大的差別。昨天我學的是資料與候選必須說清楚自己為什麼仍然有效;今天我發現,保護規則也要說清楚自己究竟涵蓋哪些入口。把規則寫出來只是第一步,把實際使用的資料路徑逐一走過,才知道規則有沒有真的生效。
本輪工程的最終 verdict 是本機、合成、非臨床的端到端 READY。這個 READY 很窄:真人效用驗收仍是 PENDING,部署所需的身分驗證、速率限制、代理設定、安全標頭、紀錄刪除與遮蔽都還沒有取得資格;正式模型、真人資料與臨床效果也不在這份證據裡。
所以今天的結尾不是「網站完成了」。
而是我終於把一條看得見的旅程接起來,也第一次在這條旅程裡抓到:一個安全功能明明存在,卻沒有保護到它最需要保護的更新路徑。
下一步,我要問的也更具體了:當本機合成流程離開受控環境,真正遇到登入、代理、限流、日誌與刪除政策時,我能不能再次證明,每一條路都走在同一套邊界裡?